/**
 * GWT Kad : Kademlia implementation for GWT
 * Copyright (C) 2012  Pierrick HYMBERT, Paul-André MISTRAL, Florian ALEXANDRE
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */
package org.kad.gwt.web.client.kademelia.transport;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.kad.gwt.web.client.kademelia.model.KadId;
import org.kad.gwt.web.client.kademelia.model.KadNode;
import org.kad.gwt.web.client.kademelia.model.rpc.KadRpcMesageType;
import org.kad.gwt.web.client.kademelia.model.rpc.KadRpcMessage;
import org.kad.gwt.web.client.kademelia.model.rpc.KadRpcRequest;
import org.kad.gwt.web.client.kademelia.model.rpc.KadRpcResponse;
import org.kad.gwt.web.client.kademelia.transport.mvp.HasKadRequestHandlers;
import org.kad.gwt.web.client.kademelia.transport.mvp.HasKadResponseHandlers;
import org.kad.gwt.web.client.kademelia.transport.mvp.KadRequestEvent;
import org.kad.gwt.web.client.kademelia.transport.mvp.KadResponseEvent;
import org.kad.gwt.web.client.mvp.AbstractHasHandlers;
import org.kad.gwt.web.client.transport.http.message.HttpMessageResponse;
import org.kad.gwt.web.client.transport.http.mvp.message.HttpServerMessageEvent;

import com.google.gwt.event.shared.EventBus;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;

/**
 * Class responsible of the kad transport layer.
 * 
 * @author Pierrick HYMBERT &lt;pierrick [dot] hymbert [at] gmail [dot] com&gt;
 */
@Singleton
public class KadTransportService extends AbstractHasHandlers implements HttpServerMessageEvent.Handler, HasKadRequestHandlers, HasKadResponseHandlers {
    /**
     * 
     */
    private static final String FIELD_SEPARATOR = "/";

    /**
     * @param eventBusIn
     *            The event bus
     */
    @Inject
    public KadTransportService(final EventBus eventBusIn) {
        super(eventBusIn);
        getEventBus().addHandler(HttpServerMessageEvent.getType(), this);
    }

    /**
     * Send an RPC message to this node.
     * 
     * @param node
     *            The target node
     * @param request
     *            The request
     */
    public <T extends KadRpcRequest> void send(final KadNode node, final T request) {
        final String message = writeMessage(request);
        final RequestBuilder builder = new RequestBuilder(RequestBuilder.POST,
                "http://" + node.getIpAddress() + ":" + node.getPort() + FIELD_SEPARATOR + node.getId() + message);
        final String payload = request.write();
        builder.setRequestData(payload);
        builder.setCallback(new RequestCallback() {
            @Override
            public void onResponseReceived(Request httpRequest, Response response) {
                handleHttpResponse(request, response.getText());
            }

            @Override
            public void onError(Request request, Throwable exception) {
                exception.printStackTrace();// FIXME
            }
        });
        try {
            builder.send();
        } catch (RequestException e) {
            e.printStackTrace();// FIXME
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onHttpServerMessage(HttpServerMessageEvent event) {
        handleHttpRequest(event.getRequest().getUri().replaceFirst("/[^/]+(/.*)", "$1"), event.getRequest().getMessage(), event.getResponse());
    }

    /**
     * Handle an http response, transform to kad message and dispatch the message to the bus.
     * 
     * @param uri
     *            The request URI
     * @param requestPayload
     *            The request message
     * @param responsePayload
     *            The response payload.
     */
    void handleHttpResponse(KadRpcRequest request, String responsePayload) {
        final String[] queryArgs = readMessage(responsePayload, FIELDS.length + 2);
        if (queryArgs == null) {
            return;
        }

        final KadRpcResponse response = request.getType().readResponse(queryArgs[queryArgs.length - 1]);
        if (!readFields(queryArgs, response)) {
            return;
        }

        KadResponseEvent.fire(this, request, response);
    }

    /**
     * Handle an http request, transform to kad message and send the response to the client.
     * 
     * @param uri
     *            The request URI
     * @param requestPayload
     *            The request message
     * @param httpMessageResponse
     * @param responseCode
     *            The response code
     * @param responsePayload
     *            The response payload.
     */
    private void handleHttpRequest(String uri, String requestPayload, HttpMessageResponse httpMessageResponse) {
        final KadRpcRequest request = readRequest(uri, requestPayload);
        if (request == null) {
            sendHttpResponse(403, "Invalid request", httpMessageResponse);
        }

        // fire the event, handlers MUST set a valid response
        KadRequestEvent messageEvent = KadRequestEvent.fire(this, request, null);
        KadRpcResponse response = messageEvent.getResponse();
        if (response != null) {
            httpMessageResponse.setResponseCode(200);
            httpMessageResponse.setMessage(writeMessage(response) + FIELD_SEPARATOR + response.write());
        }
    }

    /**
     * @param uri
     * @param requestPayload
     * @param httpMessageResponse
     * @return
     */
    private KadRpcRequest readRequest(String uri, String requestPayload) {
        final String[] queryArgs = readMessage(uri, FIELDS.length + 1);
        if (queryArgs == null) {
            return null;
        }

        // Get the associated enumeration
        final KadRpcMesageType type = KadRpcMesageType.valueOf(queryArgs[1]);
        // Decode request from payload
        final KadRpcRequest request = type.readRequest(requestPayload);

        if (!readFields(queryArgs, request)) {
            return null;
        }
        return request;
    }

    /**
     * @param uri
     * @return
     */
    private String[] readMessage(String uri, int expected) {
        // Split the query
        final String[] queryArgs = uri.split(FIELD_SEPARATOR);
        if (expected != queryArgs.length) {
            return null;
        }
        return queryArgs;
    }

    /**
     * @param response
     * @return
     */
    private String writeMessage(KadRpcMessage message) {
        StringBuilder builder = new StringBuilder();
        for (FieldSerializer fieldSerializer : FIELDS) {
            builder.append(FIELD_SEPARATOR);
            if (fieldSerializer != null) {
                builder.append(fieldSerializer.getValue(message));
            }
        }
        return builder.toString();
    }

    /**
     * @param queryArgs
     * @param message
     */
    private boolean readFields(final String[] queryArgs, final KadRpcMessage message) {
        int i = 1;
        for (FieldSerializer fieldSerializer : FIELDS) {
            String value = queryArgs[i++];
            if (value == null || value.isEmpty()) {
                return false;
            }
            if (fieldSerializer != null) {
                fieldSerializer.setValue(message, value);
            }
        }
        return true;
    }

    /**
     * @param responseContent
     * @param responseCode
     * @param httpMessageResponse
     */
    private void sendHttpResponse(int responseCode, String responseContent, HttpMessageResponse httpMessageResponse) {
        httpMessageResponse.setResponseCode(responseCode);
        httpMessageResponse.setMessage(responseContent);
    }

    interface FieldSerializer {
        String getValue(KadRpcMessage message);

        void setValue(KadRpcMessage message, String value);
    }

    private static final FieldSerializer[] FIELDS = {
            new FieldSerializer() {
                @Override
                public String getValue(KadRpcMessage message) {
                    return message.getType().name();
                }

                @Override
                public void setValue(KadRpcMessage message, String value) {
                    message.setType(KadRpcMesageType.valueOf(value));
                }
            }, // rpcType

            new FieldSerializer() {
                @Override
                public String getValue(KadRpcMessage message) {
                    return message.getId().getValue();
                }

                @Override
                public void setValue(KadRpcMessage message, String value) {
                    message.setId(new KadId(value));
                }
            },// rpcRequestId

            new FieldSerializer() {
                @Override
                public String getValue(KadRpcMessage message) {
                    return message.getSource().getValue();
                }

                @Override
                public void setValue(KadRpcMessage message, String value) {
                    message.setSource(new KadId(value));
                }
            },// rpcSource

            new FieldSerializer() {
                @Override
                public String getValue(KadRpcMessage message) {
                    return message.getTarget().getValue();
                }

                @Override
                public void setValue(KadRpcMessage message, String value) {
                    message.setTarget(new KadId(value));
                }
            },// rpcTarget

            new FieldSerializer() {
                @Override
                public String getValue(KadRpcMessage message) {
                    return String.valueOf(message.getCreationDate());
                }

                @Override
                public void setValue(KadRpcMessage message, String value) {
                    message.setCreationDate(Long.valueOf(value));
                }
            },// rpcDate
                                                  };

}
